﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;

namespace UPNP.IO
{
    partial class DeviceDom
    {
        public abstract class ElementBase
        {
            private readonly log4net.ILog _log;
            private readonly List<Exception> _schemaErrors = new List<Exception>();

            public IList<Exception> SchemaErrors
            {
                get { return _schemaErrors.AsReadOnly();  }
            }

            public abstract IEnumerable<Exception> GetSchemaErrorsRecursive();

            protected void AddSchemaError(Exception x)
            {
                Contract.Requires(x != null);

                _schemaErrors.Add(x);

                /*if (_log.IsDebugEnabled)  ### for uber debug only
                    _log.Debug("Found device schema error", x);*/
            }

            protected ElementBase()
            {
                _log = log4net.LogManager.GetLogger(GetType());
            }
        }

        public class Device : ElementBase
		{
			private readonly List<Service> _services = new List<Service>();
            private readonly List<Device> _children = new List<Device>();

            public DeviceDom OwnerDom
            {
                get;
                private set;
            }

            public Device ParentDevice
            {
                get;
                private set;
            }

            public string DeviceName
			{
				get;
				private set;
			}

			public string DeviceTypeId
			{
				get;
				private set;
			}

			public string DeviceTypeName
			{
				get { return DeviceHelpers.TypeIdToName(this.DeviceTypeId); }
			}

            public string FriendlyName
            {
                get;
                private set;
            }

            public string Manufacturer
            {
                get;
                private set;
            }

            public Uri ManufacturerUrl
            {
                get;
                private set;
            }

            public string ModelName
            {
                get;
                private set;
            }

            public string ModelDescription
            {
                get;
                private set;
            }

            public string ModelNumber
            {
                get;
                private set;
            }

            public Uri ModelUrl
            {
                get;
                private set;
            }

            public string UPC
            {
                get;
                private set;
            }

            public Uri PresentationUrl
            {
                get; 
                private set;
            }

			public IList<Device> ChildDevices
			{
				get { return _children.AsReadOnly(); }
			}

			public IList<Service> Services
			{
				get { return _services.AsReadOnly(); }
			}

            public override IEnumerable<Exception> GetSchemaErrorsRecursive()
            {
                return this.SchemaErrors.Concat(
                        this.Services.SelectMany(o => o.GetSchemaErrorsRecursive())
                    ).Concat(this.ChildDevices.SelectMany(o => o.GetSchemaErrorsRecursive()));
            }

		    internal Device(DeviceDom dom, Device parent, XmlElement deviceSchema)
		    {
                Contract.Requires(dom != null);
                ////Contract.Requires(parent != null);
                Contract.Requires(deviceSchema != null);
                Contract.Requires(deviceSchema.LocalName == "device");

                // gather values
                this.OwnerDom = dom;
                this.ParentDevice = parent;

                try
                {
                    this.DeviceName = deviceSchema.SelectSingleNodeInnerTextByLocalName("UDN", false);
                    this.DeviceTypeId = deviceSchema.SelectSingleNodeInnerTextByLocalName("deviceType", false);
                    this.FriendlyName = deviceSchema.SelectSingleNodeInnerTextByLocalName("friendlyName", false);
                    this.Manufacturer = deviceSchema.SelectSingleNodeInnerTextByLocalName("manufacturer", false);
                    this.ManufacturerUrl = deviceSchema.SelectSingleNodeInnerTextByLocalNameAsUrl("manufacturerURL", false);
                    this.ModelDescription = deviceSchema.SelectSingleNodeInnerTextByLocalName("modelDescription", false);
                    this.ModelName = deviceSchema.SelectSingleNodeInnerTextByLocalName("modelName", false);
                    this.ModelNumber = deviceSchema.SelectSingleNodeInnerTextByLocalName("modelNumber", true);
                    this.ModelUrl = deviceSchema.SelectSingleNodeInnerTextByLocalNameAsUrl("modelURL", true);
                    this.UPC = deviceSchema.SelectSingleNodeInnerTextByLocalName("UPC", true);
                    this.PresentationUrl = deviceSchema.SelectSingleNodeInnerTextByLocalNameAsUrl("presentationURL", true);

                    // create services
                    foreach (XmlElement service in deviceSchema.SelectNodes("./*[local-name()='serviceList']/*[local-name()='service']"))
                    {
                        try
                        {
                            _services.Add(new Service(
                                this.OwnerDom,
                                this,
                                service
                                ));
                        }
                        catch (Exception x)
                        {
                            AddSchemaError(new FaultyImplementationException("Failed to parse device: " + this.DeviceName, x));
                        }
                    }

                    // create children
                    foreach (XmlElement child in deviceSchema.SelectNodes("./*[local-name()='deviceList']/*[local-name()='device']"))
                    {
                        try
                        {
                            _children.Add(new Device(
                                this.OwnerDom,
                                this,
                                child
                                ));
                        }
                        catch (Exception x)
                        {
                            AddSchemaError(new FaultyImplementationException("Failed to parse device: " + this.DeviceName, x));
                        }
                    }

                }
                catch (Exception x)
                {
                    throw new FaultyImplementationException("Failed to parse device: " + this.DeviceName, x);
                }
            }
		}

        public sealed class Service : ElementBase
		{
            private readonly List<StateVariable> _vars = new List<StateVariable>();
            private readonly List<Action> _actions = new List<Action>();

            public DeviceDom OwnerDom
            {
                get;
                private set;
            }

            public Device ParentDevice
            {
                get;
                private set;
            }

            public string ServiceName
			{
				get;
				private set;
			}

			public string ServiceTypeId
			{
				get;
				private set;
			}

			public string ServiceTypeName
			{
				get { return DeviceHelpers.TypeIdToName(this.ServiceTypeId); }
			}

            public Uri ControlUrl
            {
                get;
                private set;
            }

            public Uri EventSubscriptionUrl
            {
                get;
                private set;
            }

			public IList<StateVariable> StateVariables
			{
				get { return _vars.AsReadOnly(); }
			}

			public IList<Action> Actions
			{
				get { return _actions.AsReadOnly(); }
			}

			public StateVariable GetStateVariableByName(string name)
			{
				if (string.IsNullOrEmpty(name))
					throw new ArgumentException("null or empty", "name");

				foreach (var v in _vars)
					if (name == v.VariableName)
						return v;

				return null;
			}

			public Action GetActionByName(string name)
			{
				if (string.IsNullOrEmpty(name))
					throw new ArgumentException("null or empty", "name");

				foreach (var v in _actions)
					if (name == v.ActionName)
						return v;

				return null;
			}

            public override IEnumerable<Exception> GetSchemaErrorsRecursive()
            {
                return this.SchemaErrors.Concat(
                        this.StateVariables.SelectMany(o => o.GetSchemaErrorsRecursive())
                    ).Concat(this.Actions.SelectMany(o => o.GetSchemaErrorsRecursive()));
            }

		    internal Service(DeviceDom dom, Device parent, XmlElement schema)
		    {
                Contract.Requires(dom != null);
                Contract.Requires(parent != null);
                Contract.Requires(schema != null);
                Contract.Requires(schema.LocalName == "service");

                this.OwnerDom = dom;
                this.ParentDevice = parent;

                // gather values
                try
                {
                    this.ServiceName = schema.SelectSingleNodeInnerTextByLocalName("serviceId", false);
                    this.ServiceTypeId = schema.SelectSingleNodeInnerTextByLocalName("serviceType", false);
                    this.ControlUrl = schema.SelectSingleNodeInnerTextByLocalNameAsUrl(dom.RootDeviceSchemaUrl, "controlURL", false);
                    this.EventSubscriptionUrl = schema.SelectSingleNodeInnerTextByLocalNameAsUrl(dom.RootDeviceSchemaUrl, "eventSubURL", true);

                    // create state variables
                    var vars = schema.SelectNodes(string.Format(
                        "/schema/serviceDescription[@ref=\"#{0}\"]//*[local-name()='serviceStateTable']/*[local-name()='stateVariable']",
                        this.ServiceName
                        ));

                    foreach (XmlElement v in vars)
                    {
                        try
                        {
                            _vars.Add(new StateVariable(
                                this.OwnerDom,
                                this,
                                v
                                ));
                        }
                        catch (Exception x)
                        {
                            AddSchemaError(new FaultyImplementationException("Failed to parse service of device " + parent.DeviceName + ": " + this.ServiceName, x));
                        }
                    }

                    // create actions
                    var actions = schema.SelectNodes(string.Format(
                        "/schema/serviceDescription[@ref=\"#{0}\"]//*[local-name()='actionList']/*[local-name()='action']",
                        ServiceName
                        ));

                    foreach (XmlElement a in actions)
                    {
                        try
                        {
                            _actions.Add(new Action(
                                this.OwnerDom,
                                this,
                                a
                                ));
                        }
                        catch (Exception x)
                        {
                            AddSchemaError(new FaultyImplementationException("Failed to parse service of device " + parent.DeviceName + ": " + this.ServiceName, x));
                        }
                    }

                }
                catch (FaultyImplementationException)
                {
                    // pass through
                    throw;
                }
                catch (Exception x)
                {
                    throw new FaultyImplementationException("Failed to parse service of device " + parent.DeviceName + ": " + this.ServiceName, x);
                }
            }
		}

        public sealed class StateVariable : ElementBase
		{
            private readonly List<string> _allowedValues = new List<string>();

            public DeviceDom OwnerDom
            {
                get;
                private set;
            }

            public Service ParentService
            {
                get;
                private set;
            }

            public string VariableName
			{
				get;
				private set;
			}

			public bool SendEvents
			{
				get;
                private set;
			}

            public ValueConverters.IDeviceValueConverter ValueConverter
            {
                get; 
                private set;
            }

            public Type DataType
			{
                get { return this.ValueConverter.ManagedType; }
			}

			public string DataTypeName
			{
				get;
                private set;
			}

            public string DefaultValue
            {
                get;
                private set;
            }

			public bool IsEnum
			{
				get { return ((this.AllowedValues != null) && (this.AllowedValues.Count() > 0)); }
			}

			/// <summary>
			/// allowed values. only for string.
			/// optional, may be null
			/// </summary>
			public IEnumerable<string> AllowedValues
			{
				get;
				private set;
            }

            public override IEnumerable<Exception> GetSchemaErrorsRecursive()
            {
                return this.SchemaErrors;
            }

            #region request serialization

            public string ToSoapRequest()
            {
                var sb = new StringBuilder();

                using (var sw = new StringWriter(sb))
                {
                    using (var xw = new XmlTextWriter(sw))
                    {
                        xw.Formatting = Formatting.Indented;

                        ToSoapRequest(xw);
                    }
                }

                return sb.ToString();
            }

            public void ToSoapRequest(XmlTextWriter xw)
            {
                Contract.Requires(xw != null);

                var cns = this.OwnerDom.ControlNamespace;

                xw.WriteStartElement("s", "Envelope", DeviceServiceQuery.SoapNamespace);
                xw.WriteAttributeString("xmlns", "s", null, DeviceServiceQuery.SoapNamespace);  // xmlns first
                xw.WriteAttributeString("s", "encodingStyle", DeviceServiceQuery.SoapNamespace, DeviceServiceQuery.SoapEncodingStyle);
                {
                    xw.WriteStartElement("s", "Body", DeviceServiceQuery.SoapNamespace);
                    {
                        xw.WriteStartElement("u", "QueryStateVariable", cns);
                        {
                            xw.WriteStartElement("u", "varName", cns);
                            xw.WriteString(this.VariableName);
                            xw.WriteEndElement();
                        }
                        xw.WriteEndElement();
                    }
                    xw.WriteEndElement();
                }
                xw.WriteEndElement();
            }

            #endregion

            internal StateVariable(DeviceDom dom, Service parent, XmlElement schema)
		    {
                Contract.Requires(dom != null);
                Contract.Requires(parent != null);
                Contract.Requires(schema != null);
                Contract.Requires(schema.LocalName == "stateVariable");

                this.OwnerDom = dom;
                this.ParentService = parent;

                // gather values
                try
                {
                    this.VariableName = schema.SelectSingleNodeInnerTextByLocalName("name", false);
                    this.DataTypeName = schema.SelectSingleNodeInnerTextByLocalName("dataType", false);
                    this.DefaultValue = schema.SelectSingleNodeInnerTextByLocalName("defaultValue", true);

                    // determine value converter
                    this.ValueConverter = ValueConverters.ValueConverterManager.Instance.GetDeviceValueConverter(
                        this.DataTypeName
                        );

                    // send events
                    {
                        var sendEventsAttr = schema.SelectSingleNode("./@sendEvents");

                        this.SendEvents = (sendEventsAttr == null ? false : (sendEventsAttr.Value != "no"));
                    }

                    // allowed values
                    {
                        var av = new List<string>();

                        foreach (XmlElement avx in schema.SelectNodes("./*[local-name()='allowedValueList']/*[local-name()='allowedValue']"))
                        {
                            av.Add(avx.InnerText);
                        }

                        if (av.Count > 0)
                            this.AllowedValues = av.AsReadOnly();
                    }

                }
                catch (FaultyImplementationException)
                {
                    // pass through
                    throw;
                }
                catch (Exception x)
                {
                    throw new FaultyImplementationException("Failed to parse state variable of service " + parent.ServiceName + ": " + this.VariableName, x);
                }
            }

		}

        public sealed class Action : ElementBase
		{
			private readonly List<ActionArgument> _args = new List<ActionArgument>();
		    private readonly Lazy<IList<ActionArgument>> _inArgs;
            private readonly Lazy<IList<ActionArgument>> _outArgs;

            public DeviceDom OwnerDom
            {
                get; private set;
            }

            public Service ParentService
            {
                get; private set;
            }

            public string ActionName
			{
				get; private set;
			}

			public IList<ActionArgument> Arguments
			{
				get { return _args.AsReadOnly(); }
			}

			public IList<ActionArgument> GetInArguments()
			{
			    return _inArgs.Value;
			}

			public IList<ActionArgument> GetOutArguments()
			{
			    return _outArgs.Value;
            }

            public override IEnumerable<Exception> GetSchemaErrorsRecursive()
            {
                return this.SchemaErrors.Concat(
                        this.Arguments.SelectMany(o => o.GetSchemaErrorsRecursive())
                        );
            }

            #region string serialization

            public override string ToString()
            {
                return ToString(null);
            }

            public string ToString(object[] inArgs, DynamicReturn outArgs)
            {
                Contract.Requires(inArgs == null || inArgs.Length == _inArgs.Value.Count);
                Contract.Requires(inArgs != null || outArgs == null);
                Contract.Requires(outArgs == null || outArgs.Count == _outArgs.Value.Count);

                // handle if out args are null
                if (outArgs == null)
                    return ToString(inArgs);

                // match out args
                var outArgsArray = new object[_outArgs.Value.Count];
                var counter = -1;

                foreach (var a in _outArgs.Value)
                {
                    outArgsArray[++counter] = outArgs.GetValue(a.ArgumentName);
                }

                // finalize
                return ToString(inArgs, outArgsArray);
            }

		    public string ToString(object[] inArgs, object[] outArgs = null)
            {
                Contract.Requires(inArgs == null || inArgs.Length == _inArgs.Value.Count);
                Contract.Requires(inArgs != null || outArgs == null);
                Contract.Requires(outArgs == null || outArgs.Length == _outArgs.Value.Count);

                StringBuilder sb = new StringBuilder();

                sb.Append(this.ActionName);
                sb.Append('(');

                // input arguments
                var first = true;
		        var counter = -1;

                foreach (var a in _inArgs.Value)
                {
                    if (!first)
                        sb.Append(", ");
                    else
                        first = false;

                    // add
                    sb.Append(a.ArgumentName);
                    sb.Append(": ");
                    sb.Append(a.RelatedVariable.DataTypeName);

                    // add value
                    if (inArgs != null)
                    {
                        sb.Append(" = '");
                        sb.Append(inArgs[++counter]);
                        sb.Append('\'');
                    }
                }

                // output arguments
                sb.Append("): {");

                first = true;
		        counter = -1;

                foreach (var a in _outArgs.Value)
                {
                    if (!first)
                        sb.Append(", ");
                    else
                        first = false;

                    // add
                    sb.Append(a.ArgumentName);
                    sb.Append(": ");
                    sb.Append(a.RelatedVariable.DataTypeName);

                    // add value
                    if (outArgs != null)
                    {
                        sb.Append(" = '");
                        sb.Append(outArgs[++counter]);
                        sb.Append('\'');
                    }
                }

		        sb.Append('}');

                return sb.ToString();
            }

            #endregion

            #region request serialization

            public string ToSoapRequest(object[] inputArgs)
            {
                Contract.Requires(inputArgs != null);
                Contract.Requires(inputArgs.Length == _inArgs.Value.Count);

                var sb = new StringBuilder();

                using (var sw = new StringWriter(sb))
                {
                    using (var xw = new XmlTextWriter(sw))
                    {
                        xw.Formatting = Formatting.Indented;

                        ToSoapRequest(xw, inputArgs);
                    }
                }

                return sb.ToString();
            }

            public void ToSoapRequest(XmlTextWriter xw, object[] inputArgs)
            {
                Contract.Requires(xw != null);
                Contract.Requires(inputArgs != null);
                Contract.Requires(inputArgs.Length == _inArgs.Value.Count);

                var cns = this.ParentService.ServiceTypeId;

                xw.WriteStartElement("s", "Envelope", DeviceServiceQuery.SoapNamespace);
                xw.WriteAttributeString("xmlns", "s", null, DeviceServiceQuery.SoapNamespace);  // xmlns first
                xw.WriteAttributeString("s", "encodingStyle", DeviceServiceQuery.SoapNamespace, DeviceServiceQuery.SoapEncodingStyle);
                {
                    xw.WriteStartElement("s", "Body", DeviceServiceQuery.SoapNamespace);
                    {
                        xw.WriteStartElement("u", this.ActionName, cns);

                        var counter = -1;

                        foreach (var a in _inArgs.Value)
                        {
                            xw.WriteStartElement(a.ArgumentName);
                            xw.WriteString(a.RelatedVariable.ValueConverter.ToDevice(inputArgs[++counter]));
                            xw.WriteEndElement();
                        }

                        xw.WriteEndElement();
                    }
                    xw.WriteEndElement();
                }
                xw.WriteEndElement();
            }

            #endregion

            internal Action(DeviceDom dom, Service parent, XmlElement schema)
		    {
                Contract.Requires(dom != null);
                Contract.Requires(parent != null);
                Contract.Requires(schema != null);
                Contract.Requires(schema.LocalName == "action");

                this.OwnerDom = dom;
		        this.ParentService = parent;

                // gather values
                try
                {
                    this.ActionName = schema.SelectSingleNodeInnerTextByLocalName("name", false);

                    // create arguments
                    foreach (XmlElement arg in schema.SelectNodes("./*[local-name()='argumentList']/*[local-name()='argument']"))
                    {
                        try
                        {
                            _args.Add(new ActionArgument(
                                this.OwnerDom,
                                this,
                                arg
                                ));
                        }
                        catch (Exception x)
                        {
                            AddSchemaError(new FaultyImplementationException("Failed to parse action of service " + parent.ServiceName + ": " + this.ActionName, x));
                        }
                    }

                    // prepare cached data
                    _inArgs = new Lazy<IList<ActionArgument>>(
                        () => ((
                            from a in _args
                            where a.Direction == ActionParameterDirection.In
                            select a
                            ).ToList()
                        ),
                        false
                        );

                    _outArgs = new Lazy<IList<ActionArgument>>(
                        () => ((
                            from a in _args
                            where a.Direction == ActionParameterDirection.Out
                            select a
                            ).ToList()
                        ),
                        false
                        );

                }
                catch (FaultyImplementationException)
                {
                    // pass through
                    throw;
                }
                catch (Exception x)
                {
                    throw new FaultyImplementationException("Failed to parse action of service " + parent.ServiceName + ": " + this.ActionName, x);
                }
            }
		}

		public enum ActionParameterDirection
		{
			In,
			Out
		}

        public sealed class ActionArgument : ElementBase
		{
            public DeviceDom OwnerDom
            {
                get; private set;
            }

            public Action ParentAction
            {
                get; private set;
            }

			public string ArgumentName
			{
				get; private set;
			}

			public ActionParameterDirection Direction
			{
				get; private set;
			}

			public StateVariable RelatedVariable
			{
				get; private set;
			}

            public override IEnumerable<Exception> GetSchemaErrorsRecursive()
            {
                return this.SchemaErrors.Concat(
                        this.RelatedVariable.GetSchemaErrorsRecursive()
                    );
            }

		    internal ActionArgument(DeviceDom dom, Action parent, XmlElement schema)
		    {
                Contract.Requires(dom != null);
                Contract.Requires(parent != null);
                Contract.Requires(schema != null);
                Contract.Requires(schema.LocalName == "argument");

		        this.OwnerDom = dom;
		        this.ParentAction = parent;

                // gather values
                try
                {
                    this.ArgumentName = schema.SelectSingleNodeInnerTextByLocalName("name", false);

                    this.RelatedVariable = parent.ParentService.GetStateVariableByName(
                        schema.SelectSingleNodeInnerTextByLocalName("relatedStateVariable", false)
                        );

                    this.Direction = (DeviceDom.ActionParameterDirection)Enum.Parse(
                        typeof(DeviceDom.ActionParameterDirection),
                        schema.SelectSingleNodeInnerTextByLocalName("direction", false),
                        true
                        );

                }
                catch (FaultyImplementationException)
                {
                    // pass through
                    throw;
                }
                catch (Exception x)
                {
                    throw new FaultyImplementationException(
                        "Failed to parse action argument of action " + parent.ActionName + 
                        " of service " + parent.ParentService.ServiceName + 
                        ": " + this.ArgumentName, 
                        x
                        );
                }
            }
		}

    }
}
